/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.web.taglib.common;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.Map.Entry;

import javax.servlet.jsp.JspException;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.BodyTagSupport;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.util.StringUtils;

import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.web.manager.UrlManager;
import com.genia.toolbox.web.taglib.head.HeadContainer;
import com.genia.toolbox.web.taglib.head.css.CssLink;
import com.genia.toolbox.web.taglib.head.css.CssLinkContainer;
import com.genia.toolbox.web.taglib.head.js.JsFunction;
import com.genia.toolbox.web.taglib.head.js.JsFunctionContainer;
import com.genia.toolbox.web.taglib.head.js.JsScript;
import com.genia.toolbox.web.taglib.head.js.JsScriptContainer;
import com.genia.toolbox.web.taglib.head.title.Title;

/**
 * the tag that will write all the Html page, adding informations into the head
 * section as necessary.
 */
@SuppressWarnings("serial")
public class SpringHtmlTag
    extends BodyTagSupport
    implements HtmlPageAggregator, BeanFactoryAware
{
  /**
   * the {@link BeanFactory} to use to retrieve beans.
   */
  private BeanFactory beanFactory;



  /**
   * Process the end tag for this instance. This method is invoked by the JSP
   * page implementation object on all Tag handlers.
   * <p>
   * This method will be called after returning from doStartTag. The body of the
   * action may or may not have been evaluated, depending on the return value of
   * doStartTag.
   * <p>
   * If this method returns EVAL_PAGE, the rest of the page continues to be
   * evaluated. If this method returns SKIP_PAGE, the rest of the page is not
   * evaluated, the request is completed, and the doEndTag() methods of
   * enclosing tags are not invoked. If this request was forwarded or included
   * from another page (or Servlet), only the current page evaluation is
   * stopped.
   * <p>
   * The JSP container will resynchronize the values of any AT_BEGIN and AT_END
   * variables (defined by the associated TagExtraInfo or TLD) after the
   * invocation of doEndTag().
   * 
   * @return indication of whether to continue evaluating the JSP page.
   * @throws JspException
   *           if an error occurred while processing this tag *
   * @see javax.servlet.jsp.tagext.BodyTagSupport#doAfterBody()
   */
  @Override
  public int doEndTag()
      throws JspException
  {
    try {
      final JspWriter out = pageContext.getOut();
      out.write("<html");
      String language = LocaleContextHolder.getLocale().toString();
      if (language != null && !"".equals(language)) {
        out.write(" xml:lang=\"" + language + "\"");
      }
      if (getDir() != null) {
        out.write(" dir=\"" + dir + "\"");
      }
      if (getXmlns() != null) {
        out.write(" xmlns=\"" + xmlns + "\"");
      }
      out.write(">\n");

      printHead();
      getBodyContent().writeOut(out);
      printBody();

      pageContext.getOut().write("</html>\n");
      return EVAL_PAGE;
    }
    catch (Exception e) {
      if (e instanceof JspException) {
        throw (JspException) e;
      }
      throw new JspException(e);
    }
  }

  /**
   * the <code>Log</code> variabe.
   */
  private static final Logger LOGGER = LoggerFactory.getLogger(HtmlTag.class);

  /**
   * the {@link HeadContainer} to use.
   */
  private HeadContainer headContainer;

  /**
   * the xmlns property.
   */
  private String xmlns;

  /**
   * the dir property.
   */
  private String dir;

  /**
   * the content of the head section.
   */
  private String headTagContent;

  /**
   * the content of the head section.
   */
  private String bodyTagContent;

  /**
   * the {@link BodyTag} that is enclosed by this tag.
   */
  private BodyTag bodyTag;



  /**
   * Called on a Tag handler to release state. The page compiler guarantees that
   * JSP page implementation objects will invoke this method on all tag
   * handlers, but there may be multiple invocations on doStartTag and doEndTag
   * in between.
   * 
   * @see javax.servlet.jsp.tagext.BodyTagSupport#release()
   */
  @Override
  public void release()
  {
    super.release();
    this.xmlns = null;
    this.dir = null;
    this.headTagContent = null;
    this.bodyTagContent = null;
    this.bodyTag = null;
  }



  /**
   * returns the UrlManager to use.
   * 
   * @param urlManagerName
   *          the spring name of the UrlManager to use
   * @return the UrlManager to use
   */
  private UrlManager getUrlManager(final String urlManagerName)
  {
    return (UrlManager) beanFactory.getBean(urlManagerName);
  }



  /**
   * signal sent by {@link HeadTag} in its doEndTag method.
   * 
   * @param headTag
   *          the head tag that contains the head values.
   */
  public void doAfterHeadTag(HeadTag headTag)
  {
    headTagContent = headTag.getBodyContent().getString();
  }



  /**
   * print the head of the page.
   * 
   * @throws IOException
   *           if an error occured
   * @throws BundledException
   *           if an error occured
   */
  private void printHead()
      throws IOException, BundledException
  {
    if (headTagContent == null) {
      return;
    }
    JspWriter out = pageContext.getOut();
    out.write("<head>\n");
    final Title title = getHeadContainer().getTitle();
    if (title != null && StringUtils.hasText(title.getText())) {
      out.write("<title>" + title.getText() + "</title>\n");
    }
    out.write(headTagContent);
    printCss();
    printJsLinks();
    printJsFunction(out, getHeadContainer().getInitJsFunctionContainer());
    printJsFunction(out, getHeadContainer().getTearDownJsFunctionContainer());
    out.write("</head>\n");
  }



  /**
   * print the body of the page.
   * 
   * @throws IOException
   *           if an error occured
   */
  private void printBody()
      throws IOException
  {
    if (bodyTag == null) {
      return;
    }
    JspWriter out = pageContext.getOut();
    out.write("<body");
    printJsFunctionCall("onload", getHeadContainer().getInitJsFunctionContainer());
    printJsFunctionCall("onunload", getHeadContainer().getTearDownJsFunctionContainer());
    if (bodyTag.getStyleClass() != null) {
      out.write(" class=\"");
      out.write(bodyTag.getStyleClass());
      out.write("\"");
    }
    out.write(">");
    out.write(bodyTagContent);
    out.write("</body>\n");
  }



  /**
   * signal sent by {@link BodyTag} in its doEndTag method.
   * 
   * @param bodyTag
   *          the bodyTag sending the message
   */
  public void doAfterBodyTag(BodyTag bodyTag)
  {
    bodyTagContent = bodyTag.getBodyContent().getString();
    this.bodyTag = bodyTag;
  }



  /**
   * print the callback of the body tag for the {@link JsFunctionContainer}
   * given in parameter.
   * 
   * @param callbackName
   *          the name of the callback of the body tag
   * @param jsFunctionContainer
   *          the {@link JsFunctionContainer} to write the function for
   * @throws IOException
   *           when an error occured
   */
  private void printJsFunctionCall(String callbackName, JsFunctionContainer jsFunctionContainer)
      throws IOException
  {
    if (jsFunctionContainer.getFunctions().isEmpty()) {
      return;
    }
    JspWriter out = pageContext.getOut();
    out.write(" ");
    out.write(callbackName);
    out.write("=\"");
    out.write(jsFunctionContainer.getFunctionsBaseName());
    out.write("()\"");
    jsFunctionContainer.getFunctions().clear();
  }



  /**
   * print the links that allow to declare the css of the page.
   * 
   * @throws BundledException
   *           when an error occured
   * @throws IOException
   *           when an error occured
   */
  private void printCss()
      throws BundledException, IOException
  {
    final CssLinkContainer cssLinkContainer = getHeadContainer();
    final Set<CssLink> cssLinks = cssLinkContainer.getLinks();
    Map<String, Map<Long, Collection<String>>> urls = new TreeMap<String, Map<Long, Collection<String>>>();
    for (final CssLink cssLink : cssLinks) {
      addCssUrl(urls, cssLink.getMedia(), getUrlManager(cssLink.getUrlManagerName()).getUrls(cssLink.getLink()), cssLink.getPriority());
    }
    for (final Entry<String, Map<Long, Collection<String>>> entry : urls.entrySet()) {
      final String media = CssLink.DEFAULT_CSS_MEDIA.equals(entry.getKey()) ? null : entry.getKey();
      for (final Collection<String> linkHrefCollection : entry.getValue().values()) {
        for (final String linkHref : linkHrefCollection) {
          pageContext.getOut().write("<link href=\"");
          pageContext.getOut().write(linkHref);
          pageContext.getOut().write("\" ");
          if (media != null) {
            pageContext.getOut().write("media=\"");
            pageContext.getOut().write(media);
            pageContext.getOut().write("\" ");
          }
          pageContext.getOut().write("rel=\"stylesheet\" type=\"text/css\"/>\n");
        }
      }
    }
    cssLinkContainer.getLinks().clear();
  }



  /**
   * print the links that allow to declare the javascript of the page.
   * 
   * @throws BundledException
   *           when an error occured
   * @throws IOException
   *           when an error occured
   */
  private void printJsLinks()
      throws BundledException, IOException
  {
    final JsScriptContainer jsScriptContainer = getHeadContainer();
    final Set<JsScript> jsScripts = jsScriptContainer.getScripts();
    Map<Long, Collection<String>> urls = new TreeMap<Long, Collection<String>>();
    final String timeStamp = "timeStamp=" + System.currentTimeMillis();
    for (JsScript jsScript : jsScripts) {
      Collection<String> currentUrls = getUrlManager(jsScript.getUrlManagerName()).getUrls(jsScript.getSrc());
      if (!jsScript.isCache()) {
        Collection<String> oldUrls = currentUrls;
        currentUrls = new ArrayList<String>();
        for (String url : oldUrls) {
          currentUrls.add(url + (url.contains("?") ? "&" : "?") + timeStamp);
        }
      }
      addUrlsWithPriority(urls, currentUrls, jsScript.getPriority());
    }
    for (final Collection<String> linkHrefCollection : urls.values()) {
      for (final String linkHref : linkHrefCollection) {
        pageContext.getOut().write("<script type=\"text/javascript\" src=\"");
        pageContext.getOut().write(linkHref);
        pageContext.getOut().write("\"></script>\n");
      }
    }
    jsScriptContainer.getScripts().clear();
  }



  /**
   * this method handle the construction of the list of css links, sorted by the
   * inverse value of their priority.
   * 
   * @param urls
   *          the {@link Map} that will contain the sorted css links
   * @param media
   *          the media for the given css links.
   * @param urlsToAdd
   *          the new css links to add to the {@link Map}
   * @param priority
   *          the priority of those new css links
   */
  private void addCssUrl(Map<String, Map<Long, Collection<String>>> urls, String media, Collection<String> urlsToAdd, int priority)
  {
    Map<Long, Collection<String>> urlsForCurrentMedia = urls.get(media);
    if (urlsForCurrentMedia == null) {
      urlsForCurrentMedia = new TreeMap<Long, Collection<String>>();
      urls.put(media, urlsForCurrentMedia);
    }
    addUrlsWithPriority(urlsForCurrentMedia, urlsToAdd, priority);
  }



  /**
   * add the collection of url to the {@link Map} with the correct priority.
   * 
   * @param currentUrls
   *          the already sorted urls.
   * @param urlsToAdd
   *          the urls to add
   * @param priority
   *          the priority of the url of add
   */
  private void addUrlsWithPriority(Map<Long, Collection<String>> currentUrls, Collection<String> urlsToAdd, int priority)
  {
    int index = 0;
    for (String url : urlsToAdd) {
      long currentIndex = -(priority * 1024 + index++);
      LOOP: for (Entry<Long, Collection<String>> entry : currentUrls.entrySet()) {
        if (entry.getValue().contains(url)) {
          if (currentIndex > entry.getKey()) {
            entry.getValue().remove(entry.getKey());
          }
          break LOOP;
        }
      }
      Collection<String> urlsForCurrentIndex = currentUrls.get(currentIndex);
      if (urlsForCurrentIndex == null) {
        urlsForCurrentIndex = new ArrayList<String>();
        currentUrls.put(currentIndex, urlsForCurrentIndex);
      }
      urlsForCurrentIndex.add(url);
    }
  }



  /**
   * print the script par that allows to launch javascript functions once the
   * page is loaded.
   * 
   * @param out
   *          the Writer to write the result to
   * @param jsFunctionContainer
   *          the {@link JsFunctionContainer} to write the function for.
   * @throws IOException
   *           when an error occured
   */
  private void printJsFunction(Writer out, JsFunctionContainer jsFunctionContainer)
      throws IOException
  {
    if (jsFunctionContainer.getFunctions().isEmpty()) {
      return;
    }
    final String initialisationFunction = jsFunctionContainer.getFunctionsBaseName();
    out.write("<script type=\"text/javascript\">\n");
    out.write("function " + initialisationFunction + "() {\n");
    for (final JsFunction function : jsFunctionContainer.getFunctions()) {
      out.write("\ttry {\n\t\t" + function.getName() + "();\n\t} catch(e) {\n");
      if (LOGGER.isDebugEnabled()) {
        out.write("\t\talert(e);\n");
      }
      out.write("\t}\n");
    }
    out.write("}\n");
    out.write("</script>\n");
  }



  /**
   * getter for the xmlns property.
   * 
   * @return the xmlns
   */
  public String getXmlns()
  {
    return xmlns;
  }



  /**
   * setter for the xmlns property.
   * 
   * @param xmlns
   *          the xmlns to set
   */
  public void setXmlns(String xmlns)
  {
    this.xmlns = xmlns;
  }



  /**
   * getter for the dir property.
   * 
   * @return the dir
   */
  public String getDir()
  {
    return dir;
  }



  /**
   * setter for the dir property.
   * 
   * @param dir
   *          the dir to set
   */
  public void setDir(String dir)
  {
    this.dir = dir;
  }



  /**
   * getter for the headContainer property.
   * 
   * @return the headContainer
   */
  public HeadContainer getHeadContainer()
  {
    return headContainer;
  }



  /**
   * setter for the headContainer property.
   * 
   * @param headContainer
   *          the headContainer to set
   */
  public void setHeadContainer(HeadContainer headContainer)
  {
    this.headContainer = headContainer;
  }



  /**
   * Callback that supplies the owning factory to a bean instance.
   * <p>
   * Invoked after the population of normal bean properties but before an
   * initialization callback such as
   * {@link org.springframework.beans.factory.InitializingBean#afterPropertiesSet()}
   * or a custom init-method.
   * 
   * @param beanFactory
   *          owning BeanFactory (never <code>null</code>). The bean can
   *          immediately call methods on the factory.
   * @throws BeansException
   *           in case of initialization errors
   * @see org.springframework.beans.factory.BeanInitializationException
   * @see org.springframework.beans.factory.BeanFactoryAware#setBeanFactory(org.springframework.beans.factory.BeanFactory)
   */
  public void setBeanFactory(BeanFactory beanFactory)
      throws BeansException
  {
    this.beanFactory = beanFactory;

  }

}
